#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import http.server
import socketserver
import json
import os
import webbrowser
import requests
from datetime import datetime, timedelta
import time
from typing import List, Dict, Any
import re

# 导入高德API
from backend.gaode_api import GaodeAPI

# 12306 实时查询配置
DEFAULT_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Referer": "https://kyfw.12306.cn/otn/leftTicket/init",
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Connection": "keep-alive",
}
QUERY_ENDPOINTS = [
    "https://kyfw.12306.cn/otn/leftTicket/query",
    "https://kyfw.12306.cn/otn/leftTicket/queryZ",
    "https://kyfw.12306.cn/otn/leftTicket/queryA",
    "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice",
]

# 简单内存缓存（1小时）
RAIL_CACHE = {}
# 价格缓存（10分钟）
PRICE_CACHE = {}

# 高德API配置
GAODE_API_KEY = "003df4ac18f1cca97eaf59dc90afbaa2"
_gaode_api = None

def get_gaode_api():
    """获取高德API实例"""
    global _gaode_api
    if _gaode_api is None:
        _gaode_api = GaodeAPI(GAODE_API_KEY)
    return _gaode_api

# 聚合数据API配置（航班）
JUHE_FLIGHT_APPKEY = "b0daee5c7cf963ae6458020601583575"

# 城市到IATA机场代码映射
CITY_TO_IATA = {
    '大连': 'DLC', '北京': 'PEK', '上海': 'PVG', '广州': 'CAN',
    '深圳': 'SZX', '成都': 'CTU', '重庆': 'CKG', '西安': 'XIY',
    '武汉': 'WUH', '南京': 'NKG', '杭州': 'HGH', '天津': 'TSN',
    '青岛': 'TAO', '厦门': 'XMN', '长沙': 'CSX', '郑州': 'CGO',
    '合肥': 'HFE', '济南': 'TNA', '福州': 'FOC', '南昌': 'KHN',
    '石家庄': 'SJW', '太原': 'TYN', '呼和浩特': 'HET', '沈阳': 'SHE',
    '长春': 'CGQ', '哈尔滨': 'HRB', '昆明': 'KMG', '贵阳': 'KWE',
    '南宁': 'NNG', '海口': 'HAK', '三亚': 'SYX', '兰州': 'LHW',
    '西宁': 'XNN', '银川': 'INC', '乌鲁木齐': 'URC', '拉萨': 'LXA'
}

# 周边城市映射（用于附近城市搜索与联运）
NEARBY_CITIES = {
    '大连': ['沈阳', '营口', '丹东', '鞍山'],
    '合肥': ['南京', '武汉', '郑州', '济南'],
    '北京': ['天津', '石家庄', '保定', '廊坊'],
    '上海': ['南京', '杭州', '苏州', '无锡'],
    '广州': ['深圳', '佛山', '东莞', '中山'],
    '深圳': ['广州', '东莞', '惠州', '佛山']
}

def _nearby_list(city: str) -> List[str]:
    return NEARBY_CITIES.get(city, [])

def _cache_get(key: str):
    ent = RAIL_CACHE.get(key)
    if not ent:
        return None
    ts, data = ent
    if time.time() - ts < 3600:
        return data
    return None

def _cache_set(key: str, data):
    RAIL_CACHE[key] = (time.time(), data)

def get_ground_transport_info(origin: str, destination: str, transport_type: str = 'driving') -> Dict:
    """获取地面交通信息
    
    Args:
        origin: 起点地址
        destination: 终点地址
        transport_type: 交通方式 'driving', 'transit', 'walking'
        
    Returns:
        Dict: 包含时间、距离、费用等信息
    """
    try:
        gaode_api = get_gaode_api()
        if not gaode_api:
            return {}
        
        route_info = gaode_api.get_route_info(origin, destination, transport_type)
        if route_info:
            return {
                'duration': route_info.get('duration', 0),
                'distance': route_info.get('distance', 0),
                'type': transport_type,
                'description': f"{transport_type}约{route_info.get('duration', 0)}分钟，{route_info.get('distance', 0)}公里"
            }
    except Exception as e:
        print(f"获取地面交通信息失败: {e}")
    
    return {}

def get_transport_hub_access(origin: str, hub_name: str, hub_type: str = 'airport') -> Dict:
    """获取到交通枢纽的接驳信息
    
    Args:
        origin: 起点地址
        hub_name: 交通枢纽名称
        hub_type: 枢纽类型 'airport', 'railway_station'
        
    Returns:
        Dict: 包含多种交通方式的接驳信息
    """
    try:
        gaode_api = get_gaode_api()
        if not gaode_api:
            # 返回模拟数据作为兜底
            return {
                'origin': origin,
                'destination': hub_name,
                'hub_type': hub_type,
                'options': {
                    'driving': {
                        'duration': 30,
                        'distance': 15,
                        'tolls': '0',
                        'description': f'驾车约30分钟，15公里'
                    }
                }
            }
        
        result = gaode_api.get_airport_station_access(origin, hub_name, hub_type)
        if result and result.get('options'):
            return result
        else:
            # 如果API失败，返回模拟数据
            return {
                'origin': origin,
                'destination': hub_name,
                'hub_type': hub_type,
                'options': {
                    'driving': {
                        'duration': 30,
                        'distance': 15,
                        'tolls': '0',
                        'description': f'驾车约30分钟，15公里'
                    }
                }
            }
    except Exception as e:
        print(f"获取交通枢纽接驳信息失败: {e}")
        # 返回模拟数据作为兜底
        return {
            'origin': origin,
            'destination': hub_name,
            'hub_type': hub_type,
            'options': {
                'driving': {
                    'duration': 30,
                    'distance': 15,
                    'tolls': '0',
                    'description': f'驾车约30分钟，15公里'
                }
            }
        }

def _generate_mock_train_data(origin: str, destination: str, date: str) -> List[Dict]:
    """生成模拟高铁数据"""
    import random
    
    # 根据城市距离生成不同的车次
    city_pairs = [
        ("大连", "合肥"), ("合肥", "南京"), ("北京", "上海"), 
        ("上海", "杭州"), ("广州", "深圳"), ("成都", "重庆")
    ]
    
    # 检查是否是已知的城市对
    is_known_route = any(
        (origin in pair and destination in pair) or 
        (destination in pair and origin in pair)
        for pair in city_pairs
    )
    
    if not is_known_route:
        # 未知路线，生成少量数据
        trains = [
            {
                "train_code": "G1234",
                "dep_time": "08:00",
                "arr_time": "12:30",
                "duration": "04:30",
                "from_station_name": origin,
                "to_station_name": destination,
                "seat_types": "O,M",
                "ed_left": "有",
                "yd_left": "有",
                "swz_left": "",
                "yz_left": "",
                "yw_left": "",
                "rw_left": "",
                "train_no": "G1234",
                "from_station_no": "01",
                "to_station_no": "02",
            }
        ]
    else:
        # 已知路线，生成更多数据
        trains = []
        train_codes = ["G1", "G2", "G3", "G4", "G5", "D1", "D2", "D3", "T1", "T2"]
        
        for i, code in enumerate(train_codes[:6]):
            dep_hour = 6 + i * 2
            arr_hour = dep_hour + random.randint(2, 6)
            
            trains.append({
                "train_code": code,
                "dep_time": f"{dep_hour:02d}:00",
                "arr_time": f"{arr_hour:02d}:30",
                "duration": f"{arr_hour - dep_hour:02d}:30",
                "from_station_name": origin,
                "to_station_name": destination,
                "seat_types": "O,M,A9",
                "ed_left": random.choice(["有", "无", "5", "10", "20"]),
                "yd_left": random.choice(["有", "无", "3", "8", "15"]),
                "swz_left": random.choice(["", "有", "2", "5"]),
                "yz_left": random.choice(["", "有", "10", "20"]),
                "yw_left": random.choice(["", "有", "5", "12"]),
                "rw_left": random.choice(["", "有", "3", "8"]),
                "train_no": code,
                "from_station_no": f"{i+1:02d}",
                "to_station_no": f"{i+2:02d}",
            })
    
    return trains

def _norm_city(name: str) -> str:
    if not name:
        return ''
    n = str(name).strip()
    if '(' in n:
        n = n.split('(')[0]
    
    # 处理具体的学校、车站等名称，提取城市名
    city_mappings = {
        '大连交通大学': '大连',
        '大连理工大学': '大连',
        '大连海事大学': '大连',
        '大连大学': '大连',
        '合肥南站': '合肥',
        '合肥站': '合肥',
        '合肥西站': '合肥',
        '合肥东站': '合肥',
        '北京南站': '北京',
        '北京西站': '北京',
        '北京站': '北京',
        '上海虹桥站': '上海',
        '上海南站': '上海',
        '上海站': '上海',
        '广州南站': '广州',
        '广州站': '广州',
        '深圳北站': '深圳',
        '深圳站': '深圳',
        '南京南站': '南京',
        '南京站': '南京',
        '武汉站': '武汉',
        '武汉汉口站': '武汉',
        '成都东站': '成都',
        '成都站': '成都',
        '重庆北站': '重庆',
        '重庆站': '重庆',
        '西安北站': '西安',
        '西安站': '西安',
        '郑州东站': '郑州',
        '郑州站': '郑州',
        '杭州东站': '杭州',
        '杭州站': '杭州',
    }
    
    # 先检查是否有精确匹配
    if n in city_mappings:
        return city_mappings[n]
    
    # 处理通用后缀
    for suf in ['市', '站', '南站', '北站', '东站', '西站', '火车站', '高铁站', '大学', '学院', '学校']:
        n = n.replace(suf, '')
    
    return n

def http_get_with_retry(url, params, timeout_seconds=25, max_retries=2):
    session = requests.Session()
    last_exc = None
    for attempt in range(max_retries + 1):
        try:
            resp = session.get(url, params=params, timeout=timeout_seconds)
            return resp
        except Exception as e:
            last_exc = e
            backoff = 1.5 ** attempt
            print(f"⚠️ 航班API请求失败，第{attempt+1}次：{e}，{('重试中' if attempt < max_retries else '不再重试')}…")
            if attempt < max_retries:
                time.sleep(backoff)
    raise last_exc

def get_flight_data(origin, destination, date):
    """获取机票数据（聚合数据API）"""
    try:
        dep_code = CITY_TO_IATA.get(origin, origin)
        arr_code = CITY_TO_IATA.get(destination, destination)
        
        url = "https://apis.juhe.cn/flight/query"
        params = {
            'key': JUHE_FLIGHT_APPKEY,
            'dtype': 'json',
            'departure': dep_code,
            'arrival': arr_code,
            'departureDate': date
        }
        
        print(f"✈️ 获取机票数据: {origin}({dep_code}) → {destination}({arr_code})")
        
        try:
            response = http_get_with_retry(url, params=params, timeout_seconds=25, max_retries=2)
        except Exception as e:
            print(f"❌ 航班API连续失败: {e}")
            # 兜底：返回一条可用的示例航班，避免前端空白
            fallback = [{
                'type': 'flight',
                'flight_number': 'DEMO001',
                'airline': '示例航空',
                'origin': origin,
                'destination': destination,
                'dep_time': f"{date or ''} 09:00",
                'arr_time': f"{date or ''} 11:00",
                'price': 680,
                'dep_airport': f"{origin}机场",
                'arr_airport': f"{destination}机场",
                'duration': '09:00 - 11:00',
                'source': 'fallback',
                'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                'total_time': 2.0,
                'total_price': 680,
                'transfers': 0,
                'details': [{
                    'type': 'flight', 'origin': origin, 'destination': destination,
                    'departure_time': '09:00', 'arrival_time': '11:00', 'price': 680
                }]
            }]
            return fallback
        
        if response.status_code == 200:
            result = response.json()
            
            if result.get('error_code') == 0:
                flights = []
                flight_list = result.get('result', {}).get('flightInfo', [])
                
                for item in flight_list:
                    try:
                        # 计算总时间（小时）
                        dep_time = item.get('departureTime', '')
                        arr_time = item.get('arrivalTime', '')
                        dep_date = item.get('departureDate', '')
                        arr_date = item.get('arrivalDate', '')
                        total_time = 2.0  # 默认2小时
                        try:
                            dep_dt_str = f"{dep_date} {dep_time}".strip()
                            arr_dt_str = f"{arr_date} {arr_time}".strip()
                            from datetime import datetime as _dt
                            dep_dt = _dt.strptime(dep_dt_str, '%Y-%m-%d %H:%M')
                            arr_dt = _dt.strptime(arr_dt_str, '%Y-%m-%d %H:%M')
                            if arr_dt < dep_dt:
                                arr_dt = arr_dt.replace(day=arr_dt.day) + timedelta(days=1)
                            total_minutes = int((arr_dt - dep_dt).total_seconds() // 60)
                            total_time = max(0.5, round(total_minutes / 60.0, 1))
                        except Exception:
                            pass
                        
                        flight = {
                            'type': 'flight',
                            'flight_number': item.get('flightNo', ''),
                            'airline': item.get('airlineName', ''),
                            'origin': origin,
                            'destination': destination,
                            'dep_time': f"{item.get('departureDate', '')} {item.get('departureTime', '')}",
                            'arr_time': f"{item.get('arrivalDate', '')} {item.get('arrivalTime', '')}",
                            'price': int(item.get('ticketPrice', 0)),
                            'dep_airport': item.get('departureName', ''),
                            'arr_airport': item.get('arrivalName', ''),
                            'duration': f"{item.get('departureTime', '')} - {item.get('arrivalTime', '')}",
                            'source': '聚合数据API',
                            'crawl_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
                            # 前端期望字段
                            'total_time': total_time,
                            'total_price': int(item.get('ticketPrice', 0)),
                            'transfers': 0,
                            'details': [{
                                'type': 'flight',
                                'origin': origin,
                                'destination': destination,
                                'departure_time': f"{item.get('departureTime', '')}",
                                'arrival_time': f"{item.get('arrivalTime', '')}",
                                'price': int(item.get('ticketPrice', 0)),
                                'airline': item.get('airlineName', ''),
                                'flight_number': item.get('flightNo', '')
                            }]
                        }
                        
                        if flight['price'] > 0:
                            flights.append(flight)
                            
                    except Exception as e:
                        print(f"  ❌ 解析航班条目异常: {e}")
                        continue
                
                print(f"✅ 获取到 {len(flights)} 条机票数据")
                return flights
            else:
                print(f"❌ 机票API错误: {result.get('reason', '未知')}")
                return []
        else:
            print(f"❌ 机票API请求失败: {response.status_code}")
            return []
            
    except Exception as e:
        print(f"❌ 机票API异常: {e}")
        return []

def calculate_arrival_type(route: Dict) -> str:
    """计算路线类型：日内到达或隔日到达"""
    try:
        dep_time = route.get('dep_time', '')
        arr_time = route.get('arr_time', '')
        
        if not dep_time or not arr_time:
            return 'unknown'
        
        dep_time_str = dep_time.split(' ')[-1] if ' ' in dep_time else dep_time
        arr_time_str = arr_time.split(' ')[-1] if ' ' in arr_time else arr_time
        
        from datetime import datetime as _dt
        dep_dt = _dt.strptime(dep_time_str, '%H:%M')
        arr_dt = _dt.strptime(arr_time_str, '%H:%M')
        
        if arr_dt < dep_dt:
            return 'next_day'
        else:
            return 'same_day'
            
    except Exception:
        return 'unknown'

def enhanced_sort_routes(routes: List[Dict], optimization_type: str, search_origin: str, search_destination: str) -> List[Dict]:
    """增强排序+分组：
       - 直达：transfers==0 且非附近注入(is_nearby==False)，并且首段起点=search_origin、末段终点=search_destination
       - 多式联运：type=='multimodal' 或 transfers==1
       - 分组顺序：直达 -> 日内到达 -> 隔日到达 -> 其他
    """

    def is_direct_route(r: Dict) -> bool:
        if r.get('transfers', 0) != 0:
            return False
        if r.get('is_nearby', False) or r.get('_force_nearby', False):
            return False
        first_o = r.get('origin') or (r.get('details',[{}])[0].get('origin',''))
        last_d = r.get('destination') or (r.get('details',[{}])[-1].get('destination',''))
        if _norm_city(first_o) != _norm_city(search_origin):
            return False
        if _norm_city(last_d) != _norm_city(search_destination):
            return False
        return r.get('type') in ('flight','high_speed_rail')

    # 补到达类型
    for r in routes:
        r['arrival_type'] = calculate_arrival_type(r)

    direct_routes, same_day_combos, next_day_combos, others = [], [], [], []

    for r in routes:
        if is_direct_route(r):
            r['category'] = '直达'
            r['badge'] = '直达'
            direct_routes.append(r)
            continue

        is_combo = (r.get('type') == 'multimodal' or r.get('transfers', 0) == 1)
        if is_combo:
            if r.get('arrival_type') == 'same_day':
                r['category'] = '日内到达'
                r['badge'] = '日内到达'
                same_day_combos.append(r)
            elif r.get('arrival_type') == 'next_day':
                r['category'] = '隔日到达'
                r['badge'] = '隔日到达'
                next_day_combos.append(r)
            else:
                r['category'] = '其他'
                r['badge'] = '其他'
                others.append(r)
        else:
            r['category'] = '其他'
            r['badge'] = '其他'
            others.append(r)

    # 组内排序
    def sort_key(x: Dict):
        if optimization_type == 'price':
            return x.get('total_price') if x.get('total_price') is not None else x.get('price', 0)
        return x.get('total_time', 0)

    direct_routes.sort(key=sort_key)
    same_day_combos.sort(key=sort_key)
    next_day_combos.sort(key=sort_key)
    others.sort(key=sort_key)

    # 展示标签
    for r in direct_routes + same_day_combos + next_day_combos + others:
        if r.get('arrival_type') == 'same_day':
            r['arrival_label'] = '日内到达'
        elif r.get('arrival_type') == 'next_day':
            r['arrival_label'] = '隔日到达'
        else:
            r['arrival_label'] = '未知'

    return direct_routes + same_day_combos + next_day_combos + others

def http_get_json(session: requests.Session, url: str, params: Dict[str, Any], tries=2, timeout=10):
    last = None
    for i in range(tries):
        try:
            r = session.get(url, params=params, headers=DEFAULT_HEADERS, timeout=timeout, verify=False)
            if r.status_code == 200:
                return r.json()
            last = f"HTTP {r.status_code}"
        except Exception as e:
            last = str(e)
        time.sleep(0.4 * (i + 1))
    raise RuntimeError(f"GET {url} failed: {last}")

def load_station_map(session: requests.Session) -> Dict[str, str]:
    # 多种方式解析 station_name.js；失败时兜底常见城市
    urls = [
        "https://kyfw.12306.cn/otn/resources/js/framework/station_name.js",
    ]
    text = ""
    for u in urls:
        try:
            resp = session.get(u, headers=DEFAULT_HEADERS, timeout=12, verify=False)
            if resp.status_code == 200 and "station" in resp.text:
                text = resp.text
                break
        except Exception:
            continue
    if not text:
        return {
            "北京": "BJP", "北京南": "VNP", "北京西": "BXP", "北京北": "VAP",
            "上海": "SHH", "上海虹桥": "AOH", "上海南": "SNH",
            "大连": "DLT", "合肥": "HFH", "合肥南": "ENH",
            "南京": "NJH", "南京南": "NKH", "武汉": "WHN",
            "沈阳": "SYT", "沈阳北": "SBT",
            "广州": "GZQ", "广州南": "IZQ", "深圳": "SZQ", "深圳北": "IOQ",
            "成都": "CDW", "成都东": "ICW", "重庆": "CQW", "重庆北": "CUW",
            "西安": "XAY", "西安北": "EAY", "郑州": "ZZF", "郑州东": "ZAF",
            "杭州": "HZH", "杭州东": "HGH",
        }

    raw = None
    patterns = [
        r"station_name\s*=\s*'([^']+)'",
        r"var\s+station_name\s*=\s*'([^']+)'",
        r"station_names\s*=\s*'([^']+)'",
        r"var\s+station_names\s*=\s*'([^']+)'",
    ]
    for p in patterns:
        m = re.search(p, text)
        if m:
            raw = m.group(1)
            break

    mapping: Dict[str, str] = {}
    if raw:
        for seg in raw.split("@"):
            if not seg:
                continue
            parts = seg.split("|")
            if len(parts) >= 3:
                name_cn = parts[1].strip()
                code = parts[2].strip()
                if name_cn and code:
                    mapping[name_cn] = code

    if "北京" not in mapping:
        mapping["北京"] = "BJP"
    if "上海" not in mapping:
        mapping["上海"] = "SHH"
    return mapping

def query_price(session: requests.Session, date: str, train_no: str, from_no: str, to_no: str, seat_types: str) -> Dict[str, int]:
    params = {
        "train_no": train_no,
        "from_station_no": from_no,
        "to_station_no": to_no,
        "seat_types": seat_types or "",
        "train_date": date,
    }
    def parse_money(v: Any) -> int:
        if not v:
            return 0
        m = re.search(r"([\d\.]+)", str(v))
        return int(round(float(m.group(1)))) if m else 0

    try:
        data = http_get_json(session, "https://kyfw.12306.cn/otn/leftTicket/queryTicketPrice", params, tries=2, timeout=8).get("data") or {}
    except Exception:
        data = {}
    return {
        "商务座": parse_money(data.get("A9")),
        "一等座": parse_money(data.get("M")),
        "二等座": parse_money(data.get("O")),
        "硬座":   parse_money(data.get("A1")),
        "硬卧":   parse_money(data.get("A3")),
        "软卧":   parse_money(data.get("A4")),
    }

def enrich_with_price(session: requests.Session, date: str, trains: List[Dict], limit: int = 80) -> None:
    """
    为部分车次补票价，写入:
      - t['price_map'] 各席别价格
      - t['price_min'] 用于排序与显示（优先取二等座，其次一等座，再次商务/硬座）
    """
    requests.packages.urllib3.disable_warnings()

    # 最多只补前 limit 条，避免接口太慢
    for idx, t in enumerate(trains[:max(0, limit)]):
        try:
            price_map = query_price(
                session=session,
                date=date,
                train_no=t.get("train_no", ""),
                from_no=t.get("from_station_no", ""),
                to_no=t.get("to_station_no", ""),
                seat_types=t.get("seat_types", ""),
            )
            # 仅在新结果含非零价格时才覆盖原有 price_map
            old_pm = t.get("price_map") or {}
            def _has_nonzero(pm: Dict[str, int]) -> bool:
                try:
                    return any(int(v or 0) > 0 for v in pm.values())
                except Exception:
                    return False
            if _has_nonzero(price_map):
                t["price_map"] = price_map or {}
            elif not old_pm:
                # 没有旧值且新值无效，则仍写入（保持结构），否则保留旧值
                t["price_map"] = price_map or {}
            else:
                t["price_map"] = old_pm

            # 价格优先级：二等座 > 一等座 > 商务座 > 硬座 > 硬卧 > 软卧
            pm_eff = t.get("price_map") or {}
            p = (pm_eff.get("二等座") or pm_eff.get("一等座") or pm_eff.get("商务座")
                 or pm_eff.get("硬座") or pm_eff.get("硬卧") or pm_eff.get("软卧") or 0)
            t["price_min"] = int(p or 0)
        except Exception:
            t["price_map"] = {}
            t["price_min"] = 0

    # 二次延迟重试：对第一次未取到价格但存在可售席别的车次，再尝试一次取价
    try:
        need_retry = []
        for t in trains[:max(0, limit)]:
            pm = t.get("price_map") or {}
            has_nonzero = any(int(v or 0) > 0 for v in pm.values()) if pm else False
            if not has_nonzero:
                if any(_is_available(t.get(k)) for k in ["ed_left","yd_left","swz_left","yz_left","yw_left","rw_left"]):
                    need_retry.append(t)
        if need_retry:
            time.sleep(0.8)
            for t in need_retry:
                try:
                    price_map = query_price(
                        session=session,
                        date=date,
                        train_no=t.get("train_no", ""),
                        from_no=t.get("from_station_no", ""),
                        to_no=t.get("to_station_no", ""),
                        seat_types=t.get("seat_types", ""),
                    )
                    old_pm = t.get("price_map") or {}
                    has_new = any(int(v or 0) > 0 for v in (price_map or {}).values())
                    if has_new:
                        t["price_map"] = price_map or {}
                    else:
                        # 保留旧值
                        t["price_map"] = old_pm
                    pm_eff = t.get("price_map") or {}
                    p = (pm_eff.get("二等座") or pm_eff.get("一等座") or pm_eff.get("商务座")
                         or pm_eff.get("硬座") or pm_eff.get("硬卧") or pm_eff.get("软卧") or 0)
                    if not int(t.get("price_min") or 0):
                        t["price_min"] = int(p or 0)
                except Exception:
                    pass
    except Exception:
        pass

def _is_available(v: str) -> bool:
    if not v: return False
    v = str(v).strip()
    if v == '无': return False
    if v == '有': return True
    # 数字或“5/20”这类
    try:
        # 取第一个数字
        num = int(re.findall(r'\d+', v)[0])
        return num > 0
    except Exception:
        return False

def _choose_best_seat(t: Dict) -> Dict:
    """
    返回 {'seat_label','seat_price','seat_left', 'sold_out'}，按席别优先级选择最低“有票”的席别。
    需要依赖 enrich_with_price 写入的 t['price_map'] 和 t 的余票字段（ed_left, yd_left ...）
    当价格接口不可用时，回退到基于余票/席别编码的选择，确保 seat_label 尽量有值；如果全部无票，明确标记 sold_out。
    """
    price_map = t.get('price_map', {}) or {}
    # 席别优先级：二等座 > 一等座 > 商务座 > 硬座 > 硬卧 > 软卧
    candidates = [
        ('二等座', t.get('ed_left')),
        ('一等座', t.get('yd_left')),
        ('商务座', t.get('swz_left')),
        ('硬座',   t.get('yz_left')),
        ('硬卧',   t.get('yw_left')),
        ('软卧',   t.get('rw_left')),
    ]
    best = {'seat_label': '', 'seat_price': 0, 'seat_left': '', 'sold_out': False}

    any_available = any(_is_available(left) for _, left in candidates)
    if not any_available:
        # 全部无票，直接返回 sold_out
        return {'seat_label': '无票', 'seat_price': 0, 'seat_left': '无', 'sold_out': True}

    # 1) 先按“有票”优先选（且能拿到对应价格）
    for seat_label, left in candidates:
        if _is_available(left) and price_map.get(seat_label):
            best = {'seat_label': seat_label, 'seat_price': int(price_map[seat_label] or 0), 'seat_left': left or '', 'sold_out': False}
            best['price_source'] = 'exact'
            break

    # 2) 没有“有票+可取价”的席别，再选任意“有票”的席别（价格标记为未知，避免误导）
    if not best['seat_label']:
        for seat_label, left in candidates:
            if _is_available(left):
                best = {'seat_label': seat_label, 'seat_price': 0, 'seat_left': left or '', 'sold_out': False}
                best['price_source'] = 'unknown'
                break

    # 3) 仍没有“有票”，则根据席别编码或顺序选择一个展示用席别，价格用 price_min 兜底（理论上不会到这里，因为上边已判断 any_available）
    if not best['seat_label']:
        seat_types_code = (t.get('seat_types') or '').strip()
        code_to_label = {
            'O': '二等座', 'M': '一等座', 'A9': '商务座',
            'A1': '硬座', 'A3': '硬卧', 'A4': '软卧'
        }
        picked_label = ''
        for code, label in [('O','二等座'), ('M','一等座'), ('A9','商务座'), ('A1','硬座'), ('A3','硬卧'), ('A4','软卧')]:
            if code in seat_types_code:
                picked_label = code_to_label.get(code, label)
                break
        if not picked_label:
            picked_label = '二等座'
        best = {
            'seat_label': picked_label,
            'seat_price': int(t.get('price_min') or 0),
            'seat_left': t.get({
                '二等座': 'ed_left', '一等座': 'yd_left', '商务座': 'swz_left',
                '硬座': 'yz_left', '硬卧': 'yw_left', '软卧': 'rw_left',
            }.get(picked_label, ''), '') or '',
            'sold_out': False,
            'price_source': 'min_estimate'
        }

    # 4) 如果 seat_label 已选但 seat_price 仍为 0，而 price_map 有价格，则补齐对应价格
    if best['seat_label'] and best.get('price_source') != 'unknown' and best['seat_price'] == 0 and price_map.get(best['seat_label']):
        try:
            best['seat_price'] = int(price_map.get(best['seat_label']) or 0)
            best['price_source'] = 'exact'
        except Exception:
            pass

    # 5) 最后兜底用 price_min（但不要覆盖 unknown 情况）
    if best.get('price_source') not in ('unknown',) and best['seat_price'] == 0:
        best['seat_price'] = int(t.get('price_min') or 0)
        if best['seat_price'] > 0 and not best.get('price_source'):
            best['price_source'] = 'min_estimate'
    return best

def crawl_12306_trains_realtime(date: str, origin: str, destination: str) -> List[Dict]:
    origin = _norm_city(origin); destination = _norm_city(destination)
    cache_key = f"rail:{origin}:{destination}:{date}"
    cached = _cache_get(cache_key)
    if cached is not None:
        return cached

    session = requests.Session()
    session.headers.update(DEFAULT_HEADERS)
    session.headers.update({"X-Requested-With": "XMLHttpRequest"})
    requests.packages.urllib3.disable_warnings()

    # 预热，拿到必要Cookie（RAIL_DEVICEID等）
    try:
        session.get("https://kyfw.12306.cn/otn/leftTicket/init", timeout=8, verify=False)
    except Exception:
        pass

    station_map = load_station_map(session)
    from_code = station_map.get(origin)
    to_code = station_map.get(destination)
    if not from_code or not to_code:
        print(f"❌ 站点映射失败: {origin}->{from_code}, {destination}->{to_code}")
        _cache_set(cache_key, [])
        return []

    params = {
        "leftTicketDTO.train_date": date,
        "leftTicketDTO.from_station": from_code,
        "leftTicketDTO.to_station": to_code,
        "purpose_codes": "ADULT",
    }

    trains = []
    api_success = False
    for ep in QUERY_ENDPOINTS:
        try:
            obj = _get_json_strict(session, ep, params)
            data = (obj or {}).get("data") or {}
            result_list = data.get("result") or []
            map_info = data.get("map") or {}
            for raw in result_list:
                if not raw:
                    continue
                fields = raw.split("|")
                if len(fields) < 40:
                    continue
                trains.append({
                    "train_code": fields[3],
                    "dep_time": fields[8],
                    "arr_time": fields[9],
                    "duration": fields[10],
                    "from_station_name": map_info.get(fields[6], fields[6]),
                    "to_station_name": map_info.get(fields[7], fields[7]),
                    "seat_types": fields[35],
                    "ed_left": (fields[30] or ""),
                    "yd_left": (fields[31] or ""),
                    "swz_left": (fields[32] or ""),
                    "yz_left": (fields[29] or ""),
                    "yw_left": (fields[28] or ""),
                    "rw_left": (fields[24] or ""),
                    "train_no": fields[2],
                    "from_station_no": fields[16],
                    "to_station_no": fields[17],
                })
            api_success = True
            break
        except Exception as e:
            print(f"端点失败 {ep}: {e}")
            time.sleep(0.4)
            continue
    
    # 如果所有API都失败，使用模拟数据
    if not api_success and not trains:
        print(f"⚠️ 12306 API全部失败，使用模拟数据: {origin}->{destination}")
        trains = _generate_mock_train_data(origin, destination, date)

    # 为部分车次补充真实票价（可调整 limit），复用已预热的 session
    enrich_with_price(session, date, trains, limit=min(len(trains), 80))
    _cache_set(cache_key, trains or [])
    return trains or []

def find_train_routes(origin: str, destination: str, date: str) -> List[Dict]:
    """实时查询直达高铁（页面直接展示用），类型统一为 high_speed_rail"""
    trains = crawl_12306_trains_realtime(date, origin, destination)
    routes = []
    for t in trains:
        best = _choose_best_seat(t)
        routes.append({
            "type": "high_speed_rail",
            "origin": origin,
            "destination": destination,
            "dep_time": f"{date} {t.get('dep_time','')}",
            "arr_time": f"{date} {t.get('arr_time','')}",
            "duration": t.get("duration",""),
            "price": 0 if best.get('sold_out') else best['seat_price'],
            "train_code": t.get("train_code",""),
            "train_number": t.get("train_code",""),
            "seat_selected": best['seat_label'],
            "seat_left": best['seat_left'],
            "sold_out": bool(best.get('sold_out')),
            "badge": "无票" if best.get('sold_out') else "",
            "price_source": best.get('price_source',''),
            "details": [{
                "type": "high_speed_rail",
                "train_number": t.get("train_code",""),
                "origin": t.get("from_station_name", origin),
                "destination": t.get("to_station_name", destination),
                "departure_time": t.get("dep_time",""),
                "arrival_time": t.get("arr_time",""),
                "duration": t.get("duration",""),
                "price": 0 if best.get('sold_out') else best['seat_price'],
                "seat_selected": best['seat_label'],
                "seat_left": best['seat_left'],
                "price_source": best.get('price_source',''),
                "seat_info": {
                    "二等座": t.get("ed_left",""),
                    "一等座": t.get("yd_left",""),
                    "商务座": t.get("swz_left",""),
                    "硬座": t.get("yz_left",""),
                    "硬卧": t.get("yw_left",""),
                    "软卧": t.get("rw_left",""),
                },
                "price_map": t.get("price_map", {}),
                # 供按需补价使用
                "train_no": t.get("train_no",""),
                "from_station_no": t.get("from_station_no",""),
                "to_station_no": t.get("to_station_no",""),
                "seat_types": t.get("seat_types",""),
            }],
            "transfer": "",
            "total_price": 0 if best.get('sold_out') else best['seat_price'],
            "total_duration": t.get("duration",""),
            "total_time": _parse_hours(t.get("dep_time",""), t.get("arr_time","")),
            "transfers": 0,
        })
    return routes

def _parse_hours(dep: str, arr: str) -> float:
    try:
        if not dep or not arr:
            return 0.0
        from datetime import datetime as _dt
        d1 = _dt.strptime(dep.strip(), '%H:%M')
        d2 = _dt.strptime(arr.strip(), '%H:%M')
        delta = (d2 - d1).total_seconds() / 3600.0
        if delta < 0:
            delta += 24
        return round(max(0.0, delta), 1)
    except Exception:
        return 0.0

def find_train_routes_with_earliest(origin: str, destination: str, date: str, earliest_hhmm: str) -> List[Dict]:
    """联运用：仅返回 departure_time >= earliest_hhmm 的高铁"""
    trains = crawl_12306_trains_realtime(date, origin, destination)
    routes = []
    for t in trains:
        dep = t.get("dep_time","")
        if dep and dep >= earliest_hhmm:
            best = _choose_best_seat(t)
            routes.append({
            "type": "high_speed_rail",
            "origin": origin,
            "destination": destination,
            "dep_time": f"{date} {dep}",
            "arr_time": f"{date} {t.get('arr_time','')}",
            "duration": t.get("duration",""),
            "price": 0 if best.get('sold_out') else best['seat_price'],
            "train_code": t.get("train_code",""),
            "train_number": t.get("train_code",""),
            "seat_selected": best['seat_label'],
            "seat_left": best['seat_left'],
            "sold_out": bool(best.get('sold_out')),
            "badge": "无票" if best.get('sold_out') else "",
            "price_source": best.get('price_source',''),
            "details": [{
                "type": "high_speed_rail",
                "train_number": t.get("train_code",""),
                "origin": t.get("from_station_name", origin),
                "destination": t.get("to_station_name", destination),
                "departure_time": dep,
                "arrival_time": t.get("arr_time",""),
                "duration": t.get("duration",""),
                "price": 0 if best.get('sold_out') else best['seat_price'],
                "seat_selected": best['seat_label'],
                "seat_left": best['seat_left'],
                "price_source": best.get('price_source',''),
                "seat_info": {
                    "二等座": t.get("ed_left",""),
                    "一等座": t.get("yd_left",""),
                    "商务座": t.get("swz_left",""),
                    "硬座": t.get("yz_left",""),
                    "硬卧": t.get("yw_left",""),
                    "软卧": t.get("rw_left",""),
                },
                "price_map": t.get("price_map", {}),
                "train_no": t.get("train_no",""),
                "from_station_no": t.get("from_station_no",""),
                "to_station_no": t.get("to_station_no",""),
                "seat_types": t.get("seat_types",""),
            }],
            "transfer": "",
            "total_price": 0 if best.get('sold_out') else best['seat_price'],
            "total_duration": t.get("duration",""),
            "total_time": _parse_hours(t.get("dep_time",""), t.get("arr_time","")),
            "transfers": 0,
        })
    return routes

# 常用枢纽优先级（城市 -> 机场名/车站名列表，按常用度优先）
CITY_MAJOR_AIRPORTS = {
    '南京': ['南京禄口国际机场', '南京禄口机场', '南京机场'],
    '合肥': ['合肥新桥国际机场', '合肥新桥机场', '合肥机场'],
    '大连': ['大连周水子国际机场', '大连周水子机场', '大连机场'],
}
CITY_MAJOR_STATIONS = {
    '南京': ['南京南站', '南京站'],
    '合肥': ['合肥南站', '合肥站'],
    '大连': ['大连北站', '大连站'],
}

# 预定义的机场到火车站接驳信息（避免重复API调用）
AIRPORT_TO_STATION_ROUTES = {
    '南京': {
        '南京禄口国际机场': {
            '南京南站': {'duration': 46, 'distance': 46, 'type': 'driving', 'description': '驾车约46分钟，46公里'},
            '南京站': {'duration': 50, 'distance': 48, 'type': 'driving', 'description': '驾车约50分钟，48公里'}
        }
    },
    '合肥': {
        '合肥新桥国际机场': {
            '合肥南站': {'duration': 45, 'distance': 45, 'type': 'driving', 'description': '驾车约45分钟，45公里'},
            '合肥站': {'duration': 50, 'distance': 48, 'type': 'driving', 'description': '驾车约50分钟，48公里'}
        }
    },
    '大连': {
        '大连周水子国际机场': {
            '大连北站': {'duration': 35, 'distance': 35, 'type': 'driving', 'description': '驾车约35分钟，35公里'},
            '大连站': {'duration': 40, 'distance': 38, 'type': 'driving', 'description': '驾车约40分钟，38公里'}
        }
    }
}

def build_flight_rail_combos(final_destination: str, date: str, flights: List[Dict], origin_address: str = None, origin: str = None, destination_address: str = None, optimization_type: str = 'time') -> List[Dict]:
    combos: List[Dict] = []
    fd = _norm_city(final_destination)
    print(f" 开始构建联运方案: 目标城市 {final_destination}({fd})")
    
    # 获取起点到机场的地面交通信息
    origin_to_airport = {}
    if origin_address:
        try:
            # 获取起点城市的主要机场信息
            if origin:
                city = _norm_city(origin)
                airport_name = (CITY_MAJOR_AIRPORTS.get(city) or [f"{city}机场"])[0]
                origin_to_airport = get_transport_hub_access(origin_address, airport_name, 'airport')
                if origin_to_airport:
                    origin_to_airport['chosen_airport'] = airport_name
                print(f" 获取起点到机场接驳信息: {origin_to_airport.get('options', {})}")
            else:
                print(" 警告: 未提供起点城市信息，跳过机场接驳查询")
        except Exception as e:
            print(f" 获取起点到机场接驳信息失败: {e}")
    
    combo_count = 0
    for f in flights:
        try:
            # 只考虑“飞到目的地附近城市”的航班作为联运的第一段
            if not f.get('is_nearby') or f.get('nearby_type') != 'destination':
                continue
            mid_city = f.get('nearby_city') or ''
            mid = _norm_city(mid_city)
            if not mid:
                continue
            
            # 解析航班到达时间
            flight_arr_time = f.get('arr_time', '')
            if not flight_arr_time:
                continue
                
            try:
                # 提取 "HH:MM"
                flight_time_str = flight_arr_time.split(' ')[-1] if ' ' in flight_arr_time else flight_arr_time
                from datetime import datetime as _dt
                flight_arr_dt = _dt.strptime(flight_time_str, '%H:%M')
                # 落地 + 1小时
                earliest_train_dep_dt = flight_arr_dt + timedelta(hours=1)
                earliest_train_dep_time = earliest_train_dep_dt.strftime('%H:%M')
                print(f"   搜索 {mid_city} → {final_destination} 的高铁… 最早出发 {earliest_train_dep_time}")
            except Exception as e:
                print(f"  ❌ 解析航班时间失败: {e}")
                continue
            
            # 查询中转城市 -> 终点 的高铁（按+1小时过滤）
            trains = find_train_routes_with_earliest(mid_city, final_destination, date, earliest_train_dep_time)
            if not trains:
                print(f"     ❌ 没有符合时间要求的高铁，尝试查询所有高铁")
                # 如果按时间过滤没有结果，尝试查询所有高铁
                all_trains = find_train_routes(mid_city, final_destination, date)
                if all_trains:
                    trains = all_trains[:1]  # 取第一个作为备选
                    print(f"     ✅ 找到备选高铁: {trains[0].get('train_code', '')}")
                else:
                    continue
            
            # 选一条价格最低（目前 price=0，如后续接入 queryTicketPrice 可改为按价格）
            trains.sort(key=lambda t: t.get('total_price', t.get('price', 0)))
            t = trains[0]
            train_detail = t['details'][0] if t.get('details') else {}
            
            # 计算总价与总时间（如高铁价未知则标记“待查价”而不是只用航班价）
            # 兼容性取价：优先父级，其次明细
            def _num(v):
                try:
                    return int(v)
                except Exception:
                    try:
                        return int(float(v))
                    except Exception:
                        return 0
            flight_price = _num(f.get('total_price') or f.get('price') or (f.get('details') and f['details'][0].get('price'))) 
            train_price = _num(t.get('total_price') or t.get('price') or train_detail.get('price'))
            train_price_unknown = (not t.get('sold_out', False)) and (train_price <= 0)
            if flight_price > 0 and not train_price_unknown and (t.get('sold_out', False) or train_price >= 0):
                total_price = flight_price + (0 if t.get('sold_out', False) else train_price)
                combo_price_source = 'exact'
            else:
                total_price = None
                combo_price_source = 'unknown'
            flight_time = f.get('total_time') or _parse_hours(str(f.get('dep_time',''))[-5:], str(f.get('arr_time',''))[-5:])
            train_time = t.get('total_time') or _parse_hours(train_detail.get('departure_time',''), train_detail.get('arrival_time',''))
            
            # 添加地面交通时间
            ground_transport_time = 0
            ground_transport_info = {}
            
            # 起点到机场的时间
            if origin_to_airport and origin_to_airport.get('options'):
                # 优先选择驾车，其次公交
                if 'driving' in origin_to_airport['options']:
                    ground_transport_info['to_airport'] = origin_to_airport['options']['driving']
                    ground_transport_time += origin_to_airport['options']['driving'].get('duration', 0) / 60  # 转换为小时
                elif 'transit' in origin_to_airport['options']:
                    ground_transport_info['to_airport'] = origin_to_airport['options']['transit']
                    ground_transport_time += origin_to_airport['options']['transit'].get('duration', 0) / 60  # 转换为小时
            
            # 中转城市机场到火车站的地面交通时间（仅驾车为主，选择最近的火车站：优先南站，其次普通站）
            if mid_city:
                try:
                    city = _norm_city(mid_city)
                    airport_name = (CITY_MAJOR_AIRPORTS.get(city) or [f"{city}机场"])[0]
                    candidates = (CITY_MAJOR_STATIONS.get(city) or [f"{city}南站", f"{city}火车站"])
                    # 根据优化类型选择候选车站与方式
                    chosen = None
                    for cand in candidates:
                        info = get_transport_hub_access(airport_name, cand, 'airport')
                        opts = (info or {}).get('options', {})
                        driving = opts.get('driving') or {}
                        transit = opts.get('transit') or {}
                        # 选择key：时间优先用时；价格优先费用（transit.cost），没有费用降级用时
                        if optimization_type == 'price' and transit:
                            cost_val = 0
                            try:
                                cost_val = float(transit.get('cost', '0'))
                            except Exception:
                                cost_val = 0
                            key = ('price', cost_val)
                            pick = {**transit, 'type': 'transit', 'name': cand}
                        else:
                            dur = (driving.get('duration') or 1e9)
                            key = ('time', dur)
                            pick = {**driving, 'type': 'driving', 'name': cand}
                        if pick and pick.get('duration'):
                            if (chosen is None) or (key < chosen['key']):
                                chosen = {'key': key, 'data': pick}
                    if chosen:
                        best = chosen['data']
                        ground_transport_info['airport_to_station'] = {k: best.get(k) for k in ['duration','distance','type','description','name'] if k in best}
                        ground_transport_info['airport_to_station']['airport_name'] = airport_name
                        ground_transport_time += (best.get('duration', 0) or 0) / 60
                    else:
                        print("  ⚠️ 未能获取驾车信息，使用默认接驳时间")
                        # 使用默认接驳时间
                        ground_transport_info['airport_to_station'] = {
                            'duration': 45,
                            'distance': 25,
                            'type': 'driving',
                            'description': '驾车约45分钟，25公里',
                            'name': candidates[0] if candidates else f"{city}火车站",
                            'airport_name': airport_name
                        }
                        ground_transport_time += 45 / 60
                except Exception as e:
                    print(f" 获取中转城市机场到火车站接驳信息失败: {e}")
                    # 降级默认值
                    ground_transport_info['airport_to_station'] = {
                        'duration': 45,
                        'distance': 25,
                        'type': 'driving',
                        'description': '驾车约45分钟，25公里',
                        'name': f"{city}火车站",
                        'airport_name': f"{city}机场"
                    }
                    ground_transport_time += 45 / 60
            
            # 火车站到终点的地面交通时间（到达城市的火车站 → 终点详细地址）。若没有详细地址，则不再添加此步，避免“城市间驾车”的误导。
            final_station = train_detail.get('destination') or f"{final_destination}火车站"
            # 规范化对比函数，避免“合肥南/合肥南站”这种别名产生的重复接驳
            def _norm_place(name: str) -> str:
                if not name:
                    return ''
                n = str(name).strip()
                for suf in ['站', '火车站', '高铁站', '地铁站', '站点', '(地铁站)', '（地铁站）']:
                    n = n.replace(suf, '')
                return n
            if final_destination and destination_address:
                try:
                    same_place = (_norm_place(final_station) == _norm_place(destination_address)) or (_norm_place(final_station) == _norm_place(final_destination))
                    if not same_place:
                        station_to_dest = get_transport_hub_access(final_station, destination_address, 'railway_station')
                        opts = (station_to_dest or {}).get('options', {})
                        driving = opts.get('driving') or {}
                        transit = opts.get('transit') or {}
                        pick = None
                        if optimization_type == 'price' and transit:
                            pick = {**transit, 'type': 'transit'}
                        else:
                            pick = {**driving, 'type': 'driving'}
                        if pick and pick.get('duration'):
                            ground_transport_info['from_station'] = pick
                            ground_transport_time += pick.get('duration', 0) / 60
                except Exception as e:
                    print(f" 获取火车站到终点接驳信息失败: {e}")
            # 若无详细地址或已在车站即终点，则跳过该段，避免添加“南京火车站→合肥”这类跨城驾车

            
            total_time = round((flight_time or 0) + (train_time or 0) + ground_transport_time, 1)
            
            # 构建details数组，包含地面交通信息
            details = []
            
            # 起点到机场的地面交通
            if ground_transport_info.get('to_airport'):
                to_airport = ground_transport_info['to_airport']
                details.append({
                    'type': 'ground_transport',
                    'origin': origin_address or f.get('origin'),
                    'destination': f"{f.get('origin')}机场",
                    'departure_time': '出发前',
                    'arrival_time': f.get('dep_time', ''),
                    'price': 0,  # 地面交通费用暂时设为0
                    'transport_type': to_airport.get('type', 'driving'),
                    'duration': to_airport.get('duration', 0),
                    'distance': to_airport.get('distance', 0),
                    'description': to_airport.get('description', '')
                })
            
            # 航班段
            details.append({
                'type': 'flight',
                'origin': f.get('origin'),
                'destination': mid_city,
                'departure_time': str(f.get('dep_time',''))[-5:],
                'arrival_time': str(f.get('arr_time',''))[-5:],
                'price': int(f.get('total_price') or f.get('price') or 0),
                'airline': f.get('airline'),
                'flight_number': f.get('flight_number')
            })
            
            # 中转城市机场到火车站的地面交通（仅驾车）
            if ground_transport_info.get('airport_to_station'):
                airport_to_station = ground_transport_info['airport_to_station']
                details.append({
                    'type': 'ground_transport',
                    'origin': airport_to_station.get('airport_name', f"{mid_city}机场"),
                    'destination': airport_to_station.get('name', f"{mid_city}火车站"),
                    'departure_time': str(f.get('arr_time',''))[-5:],
                    'arrival_time': '到达后',
                    'price': 0,
                    'transport_type': airport_to_station.get('type','driving'),
                    'duration': airport_to_station.get('duration', 0),
                    'distance': airport_to_station.get('distance', 0),
                    'description': airport_to_station.get('description', '')
                })
            
            # 高铁段
            details.append({
                'type': 'high_speed_rail',
                'origin': mid_city,
                'destination': final_destination,
                'departure_time': train_detail.get('departure_time',''),
                'arrival_time': train_detail.get('arrival_time',''),
                'price': int(t.get('total_price') or t.get('price') or 0),
                'train_number': train_detail.get('train_number',''),
                'seat_selected': train_detail.get('seat_selected',''),
                'seat_left': train_detail.get('seat_left',''),
                'price_source': train_detail.get('price_source',''),
                'sold_out': bool(t.get('sold_out', False)),
                'train_no': t.get('details',[{}])[0].get('train_no',''),
                'from_station_no': t.get('details',[{}])[0].get('from_station_no',''),
                'to_station_no': t.get('details',[{}])[0].get('to_station_no',''),
                'seat_types': t.get('details',[{}])[0].get('seat_types',''),
            })
            
            # 火车站到终点的地面交通（仅在提供了详细地址时展示，且使用到达城市的车站名）
            if destination_address and ground_transport_info.get('from_station'):
                from_station = ground_transport_info['from_station']
                details.append({
                    'type': 'ground_transport',
                    'origin': final_station,
                    'destination': destination_address,
                    'departure_time': train_detail.get('arrival_time',''),
                    'arrival_time': '到达后',
                    'price': 0,
                    'transport_type': from_station.get('type','driving'),
                    'duration': from_station.get('duration', 0),
                    'distance': from_station.get('distance', 0),
                    'description': from_station.get('description', '')
                })
            
            combos.append({
                'type': 'multimodal',
                'origin': f.get('origin'),
                'destination': final_destination,
                'dep_time': f.get('dep_time'),
                'arr_time': t.get('arr_time'),
                'total_price': total_price,
                'price_source': combo_price_source,
                'total_time': total_time,
                'transfers': 1,
                'transfer_city': mid_city,
                'transfer_time': f"{flight_time_str} → {train_detail.get('departure_time','')} (缓冲1小时)",
                'ground_transport_info': ground_transport_info,
                'details': details,
                'approx_price': True
            })
            combo_count += 1
            
        except Exception as e:
            print(f"  ❌ 构建联运失败: {e}")
            continue
    
    print(f"✅ 构建联运方案完成: {combo_count} 条")
    return combos

def _get_json_strict(sess, url, params):
    r = sess.get(url, params=params, headers=DEFAULT_HEADERS, timeout=8, verify=False)
    txt = r.text.strip() if hasattr(r, "text") else ""
    if r.status_code != 200 or not txt or txt[0] not in "{[":
        # 非JSON（可能被重定向/拦截），抛出异常触发下个端点
        raise RuntimeError(f"non-json resp, code={r.status_code}, head={txt[:80]}")
    return r.json()

class MyHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        static_root = os.path.dirname(__file__)
        super().__init__(*args, directory=static_root, **kwargs)
    
    def do_OPTIONS(self):
        self.send_response(200)
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()
    
    def do_GET(self):
        if self.path == '/':
            self.path = '/frontend/index.html'
        elif self.path.startswith('/api/'):
            self.handle_api_request()
            return
        super().do_GET()
    
    def do_POST(self):
        if self.path == '/api/search':
            self.handle_search_request()
        elif self.path == '/api/poi':
            self.handle_poi_request()
        elif self.path == '/api/geocode':
            self.handle_geocode_request()
        elif self.path == '/api/chat':
            self.handle_chat_request()
        elif self.path == '/api/rail_price':
            self.handle_rail_price_request()
        else:
            self.send_error(404)
    
    def handle_api_request(self):
        if self.path == '/api/status':
            self.send_api_status()
        elif self.path == '/api/cities':
            self.send_cities()
        elif self.path.startswith('/api/weather'):
            self.send_weather()
        else:
            self.send_error(404)
    
    def handle_search_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        post_data = self.rfile.read(content_length)
        
        try:
            data = json.loads(post_data.decode('utf-8'))
        except:
            data = {}
        
        # 解析参数后
        origin = data.get('origin', '北京')
        destination = data.get('destination', '上海')
        date = data.get('date') or data.get('departure_date')
        origin_address = data.get('origin_address', '')  # 起点详细地址
        destination_address = data.get('destination_address', '')  # 终点详细地址
        
        print(f"📍 地址信息: 起点地址='{origin_address}', 终点地址='{destination_address}'")

        # 归一化城市名（去掉“市/站/大学”等后缀）
        origin_norm = _norm_city(origin)
        destination_norm = _norm_city(destination)

        # 确保 optimization_type 一定有值，且是字符串
        raw_opt = data.get('optimization_type')
        optimization_type = str(raw_opt).strip().lower() if raw_opt else 'time'

        # 统一安全日期（没传就用今天）
        from datetime import datetime as _dt
        safe_date = date or _dt.now().strftime('%Y-%m-%d')
        
        print(f"\n🔍 搜索请求: {origin} → {destination} ({safe_date})")
        print("=" * 50)
        
        all_routes = []
        
        # 获取机票数据（用归一化城市名，保证 IATA 匹配）
        print("✈️ 获取机票数据...")
        flights = get_flight_data(origin_norm, destination_norm, safe_date)
        all_routes.extend(flights)
        print(f"   机票: {len(flights)} 条")

        # 获取高铁数据（同样用归一化城市名）
        print("🚂 获取高铁数据...")
        rail_routes = find_train_routes(origin_norm, destination_norm, safe_date)
        all_routes.extend(rail_routes)
        print(f"   高铁: {len(rail_routes)} 条")

        # 周边城市数据（仅用于尝试构建联运；在汇总过滤时会被剔除）
        print("🌍 获取周边城市数据...")
        nearby_origins = NEARBY_CITIES.get(origin_norm, [])
        nearby_destinations = NEARBY_CITIES.get(destination_norm, [])
        
        nearby_count = 0
        # 周边出发城市
        for nearby_origin in nearby_origins[:1]:
            if nearby_origin not in CITY_TO_IATA:
                continue
            # 使用归一化后的目的地，避免 IATA 错误
            nearby_flights = get_flight_data(nearby_origin, destination_norm, safe_date)
            for flight in nearby_flights:
                flight['is_nearby'] = True
                flight['_force_nearby'] = True
                flight['nearby_type'] = 'origin'
                flight['nearby_city'] = nearby_origin
                flight['origin'] = f"{nearby_origin}({origin_norm}附近)"
            all_routes.extend(nearby_flights)
            nearby_count += len(nearby_flights)
        
        # 周边到达城市
        for nearby_dest in nearby_destinations[:1]:
            if nearby_dest not in CITY_TO_IATA:
                continue
            # 使用归一化后的出发地
            nearby_flights = get_flight_data(origin_norm, nearby_dest, safe_date)
            for flight in nearby_flights:
                flight['is_nearby'] = True
                flight['_force_nearby'] = True
                flight['nearby_type'] = 'destination'
                flight['nearby_city'] = nearby_dest
                flight['destination'] = f"{nearby_dest}({destination_norm}附近)"
            all_routes.extend(nearby_flights)
            nearby_count += len(nearby_flights)
        
        print(f"   周边城市: {nearby_count} 条")

        # 航班+高铁 联运方案（航班到附近城市 + 高铁至终点）
        print("🚀 开始构建多式联运方案...")
        try:
            intermodal = build_flight_rail_combos(destination_norm, safe_date, all_routes, origin_address, origin_norm, destination_address, optimization_type)
            all_routes.extend(intermodal)
            print(f"   联运: {len(intermodal)} 条")
        except Exception as e:
            print(f"❌ 联运构建失败: {e}")

        # 仅保留与用户搜索一致的完整起止链路（首段起点=origin，末段终点=destination）
        def _first_origin(route):
            if route.get('origin'):
                return route['origin']
            if route.get('details'):
                return route['details'][0].get('origin','')
            return ''

        def _last_destination(route):
            if route.get('destination'):
                return route['destination']
            if route.get('details'):
                return route['details'][-1].get('destination','')
            return ''

        before_cnt = len(all_routes)
        valid_routes = []
        for r in all_routes:
            fo = _norm_city(_first_origin(r))
            ld = _norm_city(_last_destination(r))
            if fo == _norm_city(origin) and ld == _norm_city(destination):
                valid_routes.append(r)
        print(f"过滤起止后：保留 {len(valid_routes)} / {before_cnt} 条")
        all_routes = valid_routes

        # 智能排序和分组
        print("🧠 开始智能排序...")
        all_routes = enhanced_sort_routes(all_routes, optimization_type, origin, destination)

        # 统计信息
        flights_count = len([x for x in all_routes if x.get('type') == 'flight'])
        trains_count = len([x for x in all_routes if x.get('type') == 'high_speed_rail'])
        multimodal_count = len([x for x in all_routes if x.get('type') == 'multimodal'])
        nearby_count = len([x for x in all_routes if x.get('is_nearby', False)])
        
        same_day_arrival = len([x for x in all_routes if x.get('arrival_type') == 'same_day'])
        next_day_arrival = len([x for x in all_routes if x.get('arrival_type') == 'next_day'])
        
        direct_count = len([x for x in all_routes if x.get('category') == '直达'])
        same_day_count = len([x for x in all_routes if x.get('category') == '日内到达'])
        next_day_count = len([x for x in all_routes if x.get('category') == '隔日到达'])

        result = {
            'success': True,
            'routes': all_routes,
            'data': all_routes,
            'total': len(all_routes),
            'flights': flights_count,
            'trains': trains_count,
            'multimodal': multimodal_count,
            'nearby': nearby_count,
            'same_day': same_day_arrival,
            'next_day': next_day_arrival,
            'group_counts': {
                'direct': direct_count,
                'same_day': same_day_count,
                'next_day': next_day_count
            },
            'optimization_type': optimization_type,
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        }
        
        print(f"✅ 搜索完成: 总计 {len(all_routes)} 条结果")
        print(f"   ✈️ 机票: {flights_count} 条")
        print(f"   高铁: {trains_count} 条")
        print(f"   联运: {multimodal_count} 条")
        print(f"   🌍 周边(保留后统计): {nearby_count} 条")
        print(f"   日内到达: {same_day_count} 条")
        print(f"   隔日到达: {next_day_count} 条")
        print("=" * 50)
        
        self.send_json_response(result)
    
    def send_api_status(self):
        status = {
            'success': True,
            'status': 'success',
            'message': '最终版服务器运行正常',
            'version': '6.0.0',
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'features': [
                '真实机票数据获取（聚合数据API）',
                '实时12306高铁查询',
                '多式联运智能规划（时间衔接）',
                '周边城市智能对比',
                '智能排序和推荐'
            ],
            'apis': {
                'flight': 'enabled',
                'railway': 'enabled',
                'multimodal': 'enabled',
                'nearby': 'enabled',
                'smart_sort': 'enabled',
                'weather': 'mock',
                'poi': 'mock',
                'geocode': 'mock',
                'chat': 'mock'
            },
            'nearby_cities': NEARBY_CITIES
        }
        self.send_json_response(status)
    
    def send_cities(self):
        cities = list(NEARBY_CITIES.keys())
        self.send_json_response({'success': True, 'cities': cities})
    
    def send_weather(self):
        try:
            from urllib.parse import urlparse, parse_qs
            query = parse_qs(urlparse(self.path).query)
            city = (query.get('city') or ['北京'])[0]
        except Exception:
            city = '北京'
        data = {
            'success': True,
            'weather': {
                'weather': '晴',
                'temperature': 22,
                'humidity': 60,
                'wind_power': 3,
                'wind_direction': '东南风'
            },
            'city': city,
            'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        }
        self.send_json_response(data)
    
    def handle_poi_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        keyword = payload.get('keyword', '景点')
        city = payload.get('city', '北京')
        
        try:
            # 尝试使用高德API获取真实POI数据
            print(f"🔍 POI搜索: {keyword} in {city}")
            gaode_api = get_gaode_api()
            if gaode_api:
                print("  📡 调用高德API...")
                pois = gaode_api.search_poi(keyword, city)
                print(f"  📊 高德API返回 {len(pois) if pois else 0} 个结果")
                if pois:
                    # 格式化POI数据以匹配前端期望的格式
                    formatted_pois = []
                    for poi in pois[:5]:  # 只取前5个结果
                        formatted_pois.append({
                            'id': poi.get('id', ''),
                            'name': poi.get('name', ''),
                            'address': poi.get('address', ''),
                            'location': poi.get('location', ''),
                            'type': poi.get('type', keyword)
                        })
                    print(f"  ✅ 返回 {len(formatted_pois)} 个格式化POI")
                    self.send_json_response({ 'success': True, 'pois': formatted_pois, 'source': 'gaode' })
                    return
        except Exception as e:
            print(f"❌ POI搜索失败: {e}")
        
        # 如果高德API失败，返回模拟数据
        pois = [
            { 'id': '1', 'name': f'{city}{keyword}示例一', 'address': f'{city}中心区', 'location': '116.3974,39.9093', 'type': keyword },
            { 'id': '2', 'name': f'{city}{keyword}示例二', 'address': f'{city}热门商圈', 'location': '116.4053,39.9042', 'type': keyword },
            { 'id': '3', 'name': f'{city}{keyword}示例三', 'address': f'{city}特色街区', 'location': '116.4120,39.9200', 'type': keyword }
        ]
        self.send_json_response({ 'success': True, 'pois': pois, 'source': 'mock' })
    
    def handle_geocode_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        address = payload.get('address', '北京市东城区天安门')
        coordinates = { 'lng': 116.3974, 'lat': 39.9093 }
        self.send_json_response({ 'success': True, 'address': address, 'coordinates': coordinates })
    
    def handle_chat_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        message = payload.get('message', '')
        reply = f"已收到：{message}。您也可以直接输入'北京到上海'或'查询北京天气'等指令哦。"
        self.send_json_response({ 'success': True, 'response': reply })
    
    def handle_rail_price_request(self):
        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length) if content_length > 0 else b'{}'
        try:
            payload = json.loads(body.decode('utf-8'))
        except:
            payload = {}
        date = payload.get('date') or payload.get('departure_date') or ''
        train_no = payload.get('train_no','')
        from_no = payload.get('from_station_no','')
        to_no = payload.get('to_station_no','')
        seat_types = payload.get('seat_types','')
        prefer = payload.get('seat_label','')
        # 简单10分钟缓存
        key = f"price:{date}:{train_no}:{from_no}:{to_no}:{seat_types}"
        now_ts = time.time()
        hit = PRICE_CACHE.get(key)
        if hit and now_ts - hit.get('ts',0) < 600:
            pm = hit.get('pm',{})
        else:
            try:
                # 预热 session 获取必要 Cookie，提升取价成功率
                sess = requests.Session()
                try:
                    sess.headers.update(DEFAULT_HEADERS)
                    sess.headers.update({"X-Requested-With": "XMLHttpRequest"})
                    requests.packages.urllib3.disable_warnings()
                    sess.get("https://kyfw.12306.cn/otn/leftTicket/init", timeout=6, verify=False)
                except Exception:
                    pass
                pm = query_price(
                    session=sess,
                    date=date,
                    train_no=train_no,
                    from_no=from_no,
                    to_no=to_no,
                    seat_types=seat_types,
                )
                # 如果第一次全部为0，短延时后重试一次
                def _all_zero(m: Dict[str,int]) -> bool:
                    try:
                        return not m or all(int(v or 0) == 0 for v in m.values())
                    except Exception:
                        return True
                if _all_zero(pm):
                    time.sleep(0.6)
                    pm2 = query_price(
                        session=sess,
                        date=date,
                        train_no=train_no,
                        from_no=from_no,
                        to_no=to_no,
                        seat_types=seat_types,
                    )
                    # 若第二次有非零则以第二次为准
                    try:
                        if not _all_zero(pm2):
                            pm = pm2
                    except Exception:
                        pass
            except Exception:
                pm = {}
            PRICE_CACHE[key] = {'ts': now_ts, 'pm': pm}
        price = 0
        if prefer and pm:
            try:
                price = int(pm.get(prefer) or 0)
            except Exception:
                price = 0
        resp = {
            'success': True,
            'price_map': pm,
            'seat_label': prefer,
            'seat_price': price,
            'price_source': 'exact' if price > 0 else 'unknown'
        }
        self.send_json_response(resp)
    
    def send_json_response(self, data):
        self.send_response(200)
        self.send_header('Content-type', 'application/json; charset=utf-8')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
        self.send_header('Access-Control-Allow-Headers', 'Content-Type')
        self.end_headers()
        
        json_data = json.dumps(data, ensure_ascii=False, indent=2)
        self.wfile.write(json_data.encode('utf-8'))

if __name__ == '__main__':
    PORT = 5000
    
    print("=" * 60)
    print(" 启动最终版HTTP服务器")
    print("=" * 60)
    print(f"服务地址: http://localhost:{PORT}")
    print(f"前端页面: http://localhost:{PORT}/frontend/index.html")
    print("=" * 60)
    print("按 Ctrl+C 停止服务")
    print("=" * 60)
    
    try:
        try:
            webbrowser.open(f"http://localhost:{PORT}/frontend/index.html")
        except Exception:
            pass
        with socketserver.TCPServer(("", PORT), MyHTTPRequestHandler) as httpd:
            httpd.serve_forever()
    except KeyboardInterrupt:
        print("\n服务器已停止")
    except Exception as e:
        print(f"服务器启动失败: {e}")